/*
 * Copyright (C) 2013 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package org.ros.internal.xmlrpc.webserver;

import static org.jboss.netty.handler.codec.http.HttpHeaders.isKeepAlive;
import static org.jboss.netty.handler.codec.http.HttpHeaders.setContentLength;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE;
import static org.jboss.netty.handler.codec.http.HttpMethod.POST;
import static org.jboss.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1;

import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.jboss.netty.handler.codec.http.HttpVersion;
import org.jboss.netty.util.CharsetUtil;

import java.io.IOException;

/**
 * Web server handler for the Netty XMLRPC handler
 *
 * @author Keith M. Hughes
 */
public class NettyXmlRpcWebServerHandler extends SimpleChannelUpstreamHandler {

  /**
   * The web server this handler is attached to
   */
  private NettyXmlRpcWebServer webServer;

  public NettyXmlRpcWebServerHandler(NettyXmlRpcWebServer webServer) {
    this.webServer = webServer;
  }

  @Override
  public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
    Object msg = e.getMessage();
    if (msg instanceof HttpRequest) {
      handleHttpRequest(ctx, (HttpRequest) msg);
    } else {
      webServer.getLog().warn(
          String.format("Web server received unknown frame %s", msg.getClass().getName()));
    }

  }

  @Override
  public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
    webServer.channelOpened(e.getChannel());
  }

  /**
   * Handle an HTTP request coming into the server.
   *
   * @param ctx
   *          The channel context for the request.
   * @param req
   *          The HTTP request.
   *
   * @throws Exception
   */
  private void handleHttpRequest(ChannelHandlerContext ctx, HttpRequest req) throws Exception {

    if (handleWebRequest(ctx, req)) {
      // The method handled the request if the return value was true.
    } else {
      // Nothing we handle.
      webServer.getLog().warn(
          String.format("Web server has no handlers for request %s", req.getUri()));

      sendHttpResponse(ctx, req, new DefaultHttpResponse(HTTP_1_1, FORBIDDEN));
    }
  }

  /**
   * Attempt to handle an HTTP request by scanning through all registered
   * handlers.
   *
   * @param ctx
   *          The context for the request.
   * @param req
   *          The request.
   * @return True if the request was handled, false otherwise.
   */
  private boolean handleWebRequest(ChannelHandlerContext ctx, HttpRequest req) throws IOException {
    if (req.getMethod() != POST) {
      return false;
    }

    XmlRpcServerClientConnection connection =
        new XmlRpcServerClientConnection(ctx, req, webServer.getXmlRpcServer(), this);
    connection.process();

    return true;
  }

  /**
   * Send an HTTP response to the client.
   *
   * @param ctx
   *          the channel event context
   * @param req
   *          the request which has come in
   * @param res
   *          the response which is being written
   */
  public void sendHttpResponse(ChannelHandlerContext ctx, HttpRequest req, HttpResponse res) {
    // Generate an error page if response status code is not OK (200).
    if (res.getStatus().getCode() != HttpResponseStatus.OK.getCode()) {
      res.setContent(ChannelBuffers.copiedBuffer(res.getStatus().toString(), CharsetUtil.UTF_8));
      setContentLength(res, res.getContent().readableBytes());
    }

    // Send the response and close the connection if necessary.
    ChannelFuture f = ctx.getChannel().write(res);

    if (!isKeepAlive(req) || res.getStatus().getCode() != HttpResponseStatus.OK.getCode()
        || req.getMethod() == POST) {
      f.addListener(ChannelFutureListener.CLOSE);
    }
  }

  /**
   * Send a success response to the client.
   *
   * @param ctx
   *          the channel event context
   * @param req
   *          the request which has come in
   */
  private void sendSuccessHttpResponse(ChannelHandlerContext ctx, HttpRequest req) {
    DefaultHttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
    sendHttpResponse(ctx, req, res);
  }

  @Override
  public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
    webServer.getLog().error("Exception caught in the web server", e.getCause());
    e.getChannel().close();
  }

  /**
   * Send an error to the remote machine.
   *
   * @param ctx
   *          handler context
   * @param status
   *          the status to send
   */
  public void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
    HttpResponse response = new DefaultHttpResponse(HTTP_1_1, status);
    response.setHeader(CONTENT_TYPE, "text/plain; charset=UTF-8");
    response.setContent(ChannelBuffers.copiedBuffer("Failure: " + status.toString() + "\r\n",
        CharsetUtil.UTF_8));

    // Close the connection as soon as the error message is sent.
    ctx.getChannel().write(response).addListener(ChannelFutureListener.CLOSE);
  }

  /**
   * Get the webserver for the handler.
   *
   * @return the webserver for the handler
   */
  public NettyXmlRpcWebServer getWebServer() {
    return webServer;
  }
}
